#!/usr/bin/env python
#-*- coding: utf-8 -*-
# Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
#
# Use of this source code is governed by a BSD-style license
# that can be found in the LICENSE file in the root of the source
# tree. An additional intellectual property rights grant can be found
# in the file PATENTS.  All contributing project authors may
# be found in the AUTHORS file in the root of the source tree.

"""This script grabs and reports coverage information.

   It grabs coverage information from the latest Linux 32-bit build and
   pushes it to the coverage tracker, enabling us to track code coverage
   over time. This script is intended to run on the 32-bit Linux slave.

   This script requires an access.token file in the current directory, as
   generated by the request_oauth_permission.py script. It also expects a file
   customer.secret with a single line containing the customer secret. The
   customer secret is an OAuth concept and is received when one registers the
   application with the App Engine running the dashboard.

   The script assumes that all coverage data is stored under
   /home/<build bot user>/www.
"""

import os
import re
import sys

import constants
import dashboard_connection


class FailedToParseCoverageHtml(Exception):
  pass


class CouldNotFindCoverageDirectory(Exception):
  pass


def _find_latest_build_coverage(www_directory_contents, coverage_www_dir,
                                directory_prefix):
  """Finds the most recent coverage directory in the directory listing.

     We assume here that build numbers keep rising and never wrap around.

     Args:
       www_directory_contents: A list of entries in the coverage directory.
       coverage_www_dir: The coverage directory on the bot.
       directory_prefix: Coverage directories have the form <prefix><number>,
           and the prefix is different on different bots. The prefix is
           generally the builder name, such as Linux32DBG.

     Returns:
       The most recent directory name.

     Raises:
       CouldNotFindCoverageDirectory: if we failed to find coverage data.
  """

  found_build_numbers = []
  for entry in www_directory_contents:
    match = re.match(directory_prefix + '(\d+)', entry)
    if match is not None:
      found_build_numbers.append(int(match.group(1)))

  if not found_build_numbers:
    raise CouldNotFindCoverageDirectory('Error: Found no directories %s* '
                                        'in directory %s.' %
                                         (directory_prefix, coverage_www_dir))

  most_recent = max(found_build_numbers)
  return directory_prefix + str(most_recent)


def _grab_coverage_percentage(label, index_html_contents):
  """Extracts coverage from a LCOV coverage report.

     Grabs coverage by assuming that the label in the coverage HTML report
     is close to the actual number and that the number is followed by a space
     and a percentage sign.
  """
  match = re.search('<td[^>]*>' + label + '</td>.*?(\d+\.\d) %',
                    index_html_contents, re.DOTALL)
  if match is None:
    raise FailedToParseCoverageHtml('Missing coverage at label "%s".' % label)

  try:
    return float(match.group(1))
  except ValueError:
    raise FailedToParseCoverageHtml('%s is not a float.' % match.group(1))


def _report_coverage_to_dashboard(dashboard, line_coverage, function_coverage,
                                  branch_coverage, report_category):
  parameters = {'line_coverage': '%f' % line_coverage,
                'function_coverage': '%f' % function_coverage,
                'branch_coverage': '%f' % branch_coverage,
                'report_category': report_category,
               }

  dashboard.send_post_request(constants.ADD_COVERAGE_DATA_URL, parameters)


def _main(report_category, directory_prefix):
  """Grabs coverage data from disk on a bot and publishes it.

     Args:
       report_category: The kind of coverage to report. The dashboard
           application decides what is acceptable here (see
           dashboard/add_coverage_data.py for more information).
      directory_prefix: This bot's coverage directory prefix. Generally a bot's
          coverage directories will have the form <prefix><build number>,
          like Linux32DBG_345.
  """
  dashboard = dashboard_connection.DashboardConnection(constants.CONSUMER_KEY)
  dashboard.read_required_files(constants.CONSUMER_SECRET_FILE,
                                constants.ACCESS_TOKEN_FILE)

  coverage_www_dir = constants.BUILD_BOT_COVERAGE_WWW_DIRECTORY
  www_dir_contents = os.listdir(coverage_www_dir)
  latest_build_directory = _find_latest_build_coverage(www_dir_contents,
                                                       coverage_www_dir,
                                                       directory_prefix)

  index_html_path = os.path.join(coverage_www_dir, latest_build_directory,
                                 'index.html')
  index_html_file = open(index_html_path)
  whole_file = index_html_file.read()

  line_coverage = _grab_coverage_percentage('Lines:', whole_file)
  function_coverage = _grab_coverage_percentage('Functions:', whole_file)
  branch_coverage = _grab_coverage_percentage('Branches:', whole_file)

  _report_coverage_to_dashboard(dashboard, line_coverage, function_coverage,
      branch_coverage, report_category)


def _parse_args():
  if len(sys.argv) != 3:
    print ('Usage: %s <coverage category> <directory prefix>\n\n'
           'The coverage category describes the kind of coverage you are '
           'uploading. Known acceptable values are small_medium_tests and'
           'large_tests. The directory prefix is what the directories in %s '
           'are prefixed on this bot (such as Linux32DBG_).' %
               (sys.argv[0], constants.BUILD_BOT_COVERAGE_WWW_DIRECTORY))
    return (None, None)
  return (sys.argv[1], sys.argv[2])


if __name__ == '__main__':
  category, dir_prefix = _parse_args()
  if category:
    _main(category, dir_prefix)

